package com.inspectortime.webapp.inspector;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;

import com.inspectortime.calendar.MultiInspectorCalendar;
import com.inspectortime.domain.Inspection;
import com.inspectortime.domain.Inspector;
import com.inspectortime.domain.InspectorCompany;
import com.inspectortime.domain.OptionalService;
import com.inspectortime.domain.Property;
import com.inspectortime.domain.RealEstateAgent;
import com.inspectortime.domain.User;
import com.inspectortime.domain.type.Address;
import com.inspectortime.domain.type.FoundationType;
import com.inspectortime.domain.type.InspectionStatus;
import com.inspectortime.domain.type.LockboxType;
import com.inspectortime.domain.type.PaymentStatus;
import com.inspectortime.domain.type.PresentAtInspectionType;
import com.inspectortime.domain.type.PropertyType;
import com.inspectortime.domain.type.State;
import com.inspectortime.mail.EmailMessageUtils;
import com.inspectortime.mail.service.AbstractMailMessage;
import com.inspectortime.mail.service.MailService;
import com.inspectortime.repository.Factory;
import com.inspectortime.repository.Repository;
import com.inspectortime.service.ZipCodeSearchService;
import com.inspectortime.util.SpringValidatorUtils;
import com.inspectortime.webapp.InspectionCriteria;
import com.inspectortime.webapp.util.CustomStateEditor;
import com.inspectortime.webapp.util.DomainUtils;
import com.inspectortime.webapp.util.FlexibleNumberPropertyEditor;
import com.inspectortime.webapp.util.HttpSessionUtils;

public class EditInspectionFormController extends SimpleFormController {

	private static Log log = LogFactory.getLog(EditInspectionFormController.class);
	private Repository repository;
	private Factory factory;
	private ZipCodeSearchService zipCodeSearchService;
	private EditReferringRealtorFormValidator realtorFormValidator;
	private MailService mailService;
	
	protected Map referenceData(HttpServletRequest request) throws Exception {
		Map<String, Object> data = new HashMap<String, Object>();
		
		data.put("foundationTypes", FoundationType.values());
		data.put("propertyTypes", PropertyType.values());
		data.put("lockboxTypes", LockboxType.values());
		data.put("presentAtInspectionOptions", PresentAtInspectionType.values());
		data.put("inspectionStatuses", prepareInspectionStatuses());
		data.put("paymentStatuses", PaymentStatus.values());

		InspectorCompany company = HttpSessionUtils.retrieveLoggedInCompany(request, repository);
		data.put("inspectors", company.getInspectors());
		data.put("states", findServedStates(company));
		data.put("allStates", State.values());
		data.put("referringRealtors", company.getReferringAgents());
		
		MultiInspectorCalendar consolidatedCalendar = new MultiInspectorCalendar(60, company.getInspectors());
		consolidatedCalendar.markScheduledAsUnavailable();
		data.put("inspectorDays", consolidatedCalendar.dayListByInspectorId());
		data.put("todayCal", new GregorianCalendar());
		data.put("inspectorCompany", company);
		
		return data;
	}

	protected Object formBackingObject(HttpServletRequest request)
			throws Exception {

		InspectionCriteria criteria = new InspectionCriteria();
		
		String id = request.getParameter("id");
		
		Inspection inspection = null;
		if (!StringUtils.hasText(id)) {
			inspection = factory.createInspection();
			inspection.setInspector(factory.createInspector());
		} else {
			inspection = repository.loadInspection(id);
		}
		
		//If inspection has a second home buyer, add it to criteria or else 
		//create an empty one that can be bound to the form fields
		if (inspection.getSecondHomeBuyer() != null) {
			criteria.setSecondHomeBuyer(inspection.getSecondHomeBuyer());
		} else {
			criteria.setSecondHomeBuyer(factory.createHomeBuyer());
		}

		// workaround: We need a property address object for the form
		Property property = inspection.getProperty();
		if (property.getLocation() == null) {
			property.setLocation(new Address());
		}

		criteria.setInspection(inspection);
		criteria.setReferringRealEstateAgent(factory.createRealEstateAgent());
		
		// workaround: We need a company for the validator
		criteria.setInspectorCompany(inspection.getInspector().getCompany());
		
		return criteria;
	}
	
	
	protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
		binder.registerCustomEditor(State.class, new CustomStateEditor());
		
		binder.registerCustomEditor(Integer.class, "inspection.property.constructionYear",
				new FlexibleNumberPropertyEditor("0000", Integer.class));		
		binder.registerCustomEditor(Float.class, "inspection.property.listedPrice",
				new FlexibleNumberPropertyEditor("###,###,##0", Float.class));		
		binder.registerCustomEditor(Double.class, "inspection.property.squareFeet",
				new FlexibleNumberPropertyEditor("###,###,##0", Double.class));		
		binder.registerCustomEditor(Double.class, "inspection.enteredInspectionFee",
				new FlexibleNumberPropertyEditor("###,###,##0", Double.class));		

	}

	
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {

		InspectionCriteria criteria = (InspectionCriteria) command;
		Inspection inspection =  criteria.getInspection();
		
		// If we got here we passed validation, so update status
		// if it's null (ie inspector is creating an inspection)
		if (inspection.getInspectionStatus() == null) {
			inspection.setInspectionStatus(InspectionStatus.SCHEDULED);
			User user = HttpSessionUtils.retrieveLoggedInUser(request, repository);
			log.info("Scheduler " + user.getEmail() + " created inspection for inspector " + inspection.getInspector().getEmail() + " of " + inspection.getInspector().getCompany().getShortName() + " at " + inspection.getInspectionTime());
		}
		
		//the validator will ensure that all fields are properly filled out for the second home buyer, if any of them are. 
		//so, if first name is filled out, assume user the rest of the second buyer info is valid
		if (StringUtils.hasText(criteria.getSecondHomeBuyer().getName().getFirst())) {
			inspection.setSecondHomeBuyer(criteria.getSecondHomeBuyer());
		} else {
			inspection.setSecondHomeBuyer(null);
		}
		
		// Real estate agent logic
		if (StringUtils.hasText(criteria.getAddRealtorInfo())) {
			//If user entered new real estate agent, add to company's agents
			inspection.setReferringAgent(criteria.getReferringRealEstateAgent());
			inspection.getInspector().getCompany().getReferringAgents().add(criteria.getReferringRealEstateAgent());
			
		} else {
			// Now check if user selected a pre-existing real estate agent
			if (StringUtils.hasText(criteria.getReferringAgentId())) {
				// the normal realtor dropdown for non-javascript enabled browsers
				inspection.setReferringAgent(repository.loadRealEstateAgent(criteria.getReferringAgentId()));
				
			} else if  (StringUtils.hasText(criteria.getSelectedRealtor())) {
				// the auto-complete input field
				inspection.setReferringAgent(DomainUtils.getExistingRealtorByAutoCompletionName(criteria.getSelectedRealtor(), inspection.getInspector().getCompany().getReferringAgents()));
			}
		}
		
		
		// Calculate inspection fee (and message user if it changes)
		String messageStr = "";
		double oldCalculatedFee = inspection.getInspectionFee();
		double newCalculatedFee = inspection.getInspector().getCompany().calculateInspectionFee(inspection);
		if (oldCalculatedFee != newCalculatedFee) {
			inspection.setInspectionFee(inspection.getInspector().getCompany().calculateInspectionFee(inspection));
			messageStr = "&message=Calculated new inspection fee: " + DecimalFormat.getCurrencyInstance().format(inspection.getInspectionFee());			
		}
		
		Serializable id = inspection.save();
		
		//Send confirmation emails
		try {
			if (criteria.isSendEmailToBuyer()) {
				AbstractMailMessage message = EmailMessageUtils.prepareBuyerConfirmationMessage(inspection, request);
				this.mailService.send(message);			
			}
	
			if (criteria.isSendEmailToInspector()) {
				AbstractMailMessage message2 = EmailMessageUtils.prepareInspectorConfirmationMessage(inspection, request);
				this.mailService.send(message2);
			}
			
			if (criteria.isSendEmailToRealtor() && inspection.getReferringAgent() != null && StringUtils.hasText(inspection.getReferringAgent().getEmail())) {
				AbstractMailMessage message3 = EmailMessageUtils.prepareRealtorThankYouMessage(inspection, request);
				this.mailService.send(message3);
			}
		} catch (Exception e) {
			//Let's not show an error page if the email fails. Just log it and move on
			log.error("Exception thrown trying to send email for inspection id: " + inspection.getId(), e);
		}
		
		
		ModelAndView mv = new ModelAndView(this.getSuccessView() + "?id=" + id + messageStr);
		return mv;
	}

	
	protected void onBind(HttpServletRequest request,
			Object command, BindException errors) throws Exception {

		InspectionCriteria criteria = (InspectionCriteria) command;
		Inspection inspection = criteria.getInspection();
		
		// Manually parse and bind 'manualBindingDateTimeInspector'
		SimpleDateFormat fmt = new SimpleDateFormat("MMM d, yyyy hh:mm:ss a");
		String param = request.getParameter("manualBindingDateTimeInspector");
		if (param.equalsIgnoreCase("UNASSIGNED")) {
			errors.reject("timeslot.required", "Please select Timeslot and Inspector");
		} else {
			String[] params = param.split("\\|");
			Date newInspectionTime = fmt.parse(params[0]);
			String inspectorId = params[1];

			// Store new inspection time
			inspection.setInspectionTime(newInspectionTime);

			// Changed inspector? 
			if (!inspectorId.equals(inspection.getInspector().getId())) {
				Inspector newInspector = repository.loadInspector(inspectorId);
				inspection.setInspector(newInspector);
			}
		}

		// Manually bind the optionalService.id.X checkboxes by index
		InspectorCompany company = HttpSessionUtils.retrieveLoggedInCompany(request, repository);
		for (int i = 0; i < company.getOptionalServices().size(); i++) {
			String param2 = request.getParameter("optionalService.id." + i);
			if (param2 != null && param2.equals("on")) {
				
				// Add service corresponding to the checked checkbox
				OptionalService service = company.getOptionalServices().get(i);
				if (service == null) {
					throw new RuntimeException("Optional service for company " + company.getShortName() + " at index " + i + " not found, this should not happen.");
				}
				if (!inspection.getOptionalServices().contains(service)) {
					inspection.getOptionalServices().add(service);
				}
			} else {
				
				// Optionally remove service corresponding to the un-checked checkbox
				OptionalService service = company.getOptionalServices().get(i);
				if (service != null) {
					inspection.getOptionalServices().remove(service);
				}
			}
		}		
		
		SpringValidatorUtils.logValidationErrors(log, errors);
	}
	
	
	
	protected void onBindAndValidate(HttpServletRequest request,
			Object command, BindException errors) throws Exception {
		
		InspectionCriteria criteria = (InspectionCriteria) command;
		
		
		//Explicitly handle realtor logic
		if (StringUtils.hasText(criteria.getSelectedRealtor())) {
			
			InspectorCompany company = HttpSessionUtils.retrieveLoggedInCompany(request, repository);
			Collection<RealEstateAgent> existingAgents = company.getReferringAgents();
		
			//If the user entered a realtor in the auto-complete form, verify that the realtor actually exists
			RealEstateAgent realtor = DomainUtils.getExistingRealtorByAutoCompletionName(criteria.getSelectedRealtor(), existingAgents);

			if (realtor == null) {
				errors.rejectValue("selectedRealtor", "notFound");
			}
		} else if (StringUtils.hasText(criteria.getAddRealtorInfo()))  {
				errors.pushNestedPath("referringRealEstateAgent");
				this.realtorFormValidator.validate(criteria.getReferringRealEstateAgent(), errors);
				errors.popNestedPath();
		}
		SpringValidatorUtils.logValidationErrors(log, errors);
	}
	
	
	
	private List<State> findServedStates(InspectorCompany inspectorCompany) {
		Set<String> nearbyStates = zipCodeSearchService.findStatesByZipCodeRadiusInMiles(inspectorCompany.getAddress().getZipCode(), inspectorCompany.getMaxMilesServed());
		return prepareServedStatesMap(nearbyStates);
		
	}

	private List<State> prepareServedStatesMap(Set<String> nearbyStates) {
		List<State> servedStates = new ArrayList<State>(nearbyStates.size());
		for (String stateCode : nearbyStates){
			State blu = State.valueOf(stateCode);
			servedStates.add(State.valueOf(stateCode));
		}
		return servedStates;
	}
	
	/**
	 * Removes any Inspection Statuses that should not be made available for selection
	 * by the user
	 * 
	 */
	private List<InspectionStatus> prepareInspectionStatuses() {
		List<InspectionStatus> inspectionStatuses = new ArrayList<InspectionStatus>();
		for (InspectionStatus status : InspectionStatus.values()) {
			if (status != InspectionStatus.AUTO_EXPIRED) {
				inspectionStatuses.add(status);
			}
		}
		return inspectionStatuses;
	}
	
	public void setZipCodeSearchService(ZipCodeSearchService zipCodeSearchService) {
		this.zipCodeSearchService = zipCodeSearchService;
	}
	
	public void setRealtorFormValidator(
			EditReferringRealtorFormValidator realtorFormValidator) {
		this.realtorFormValidator = realtorFormValidator;
	}
	
	public void setMailService(MailService mailService) {
		this.mailService = mailService;
	}
	
	public void setRepository(Repository repository) {
		this.repository = repository;
	}

	public void setFactory(Factory repository) {
		this.factory = repository;
	}
}
